package com.hilton.tigershark.downloads;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
import android.widget.RemoteViews;

import com.hilton.tigershark.R;
import com.hilton.tigershark.downloads.Downloads.DownloadStatus;

public class DownloadService extends Service {
    private static final String TAG = "DownloadService";
    public static final String DOWNLOAD_TARGET = "download";
    private static final int IDLE_TIMEOUT = 60 * 1000; // One minute
    private static final int MSG_STOP_SELF = 10;
    private InternalWorker mInternalWorker;
    private NotificationManager mNotificationManager;
    private IBinder mBinder;
    private HashMap<Integer, Integer> mProgressTable;
    private boolean mServiceInUse;
    
    private Handler mServiceHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_STOP_SELF:
                Log.e(TAG, "good bye crule worold");
                stopSelf();
                break;
            default:
                break;
            }
        }
    };
    
    @Override
    public void onCreate() {
        super.onCreate();
        Log.e(TAG, "onCreate creating");
        mInternalWorker = new InternalWorker();
        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        mBinder = new ServiceStub(this);
        mProgressTable = new HashMap<Integer, Integer>();
        mServiceInUse = true;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy destroying");
        mInternalWorker.terminate();
        mInternalWorker = null;
        Log.e(TAG, "on destory finished");
        mServiceInUse = false;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onstart command " + intent);
        if (intent == null) {
            return idleAndQuit();
        }
        DownloadInfo info = intent.getParcelableExtra(DOWNLOAD_TARGET);
        if (info == null || info.empty()) {
            return idleAndQuit();
        }
        mServiceHandler.removeMessages(MSG_STOP_SELF);
        mInternalWorker.executeDownload(info);
        mServiceInUse = true;
        return START_STICKY;
    }

    private int idleAndQuit() {
	if (mServiceInUse) {
	    return START_NOT_STICKY;
	}
        Log.e(TAG, "idleAndQuit stop self in " + IDLE_TIMEOUT);
        mServiceHandler.removeMessages(MSG_STOP_SELF);
        mServiceHandler.sendEmptyMessageDelayed(MSG_STOP_SELF, IDLE_TIMEOUT);
        return START_NOT_STICKY;
    }

    public int getProgress(int id) {
	return mProgressTable.get(id);
    }
    
    private void updateProgress(int id, int current) {
	mProgressTable.put(id, current);
    }
    
    @Override
    public boolean onUnbind(Intent intent) {
	mServiceInUse = false;
        return true;
    }

    @Override
    public IBinder onBind(Intent intent) {
	mServiceInUse = true;
        return mBinder;
    }
    
    @Override
    public void onRebind(Intent intent) {
	mServiceInUse = true;
    }

    public class InternalWorker extends HandlerThread implements DownloadsObserver {
	private static final String TAG = "InternalWorker";
        private static final int MSG_DOWNLOAD = 100;
        private static final int MSG_STOP_SERVICE = 101;
        
        private static final int THREAD_POOL_SIZE = 10;
        private static final long TERMINATION_TIMEOUT_IN_SECONDS = 20;
        private Handler mHandler;
        private ExecutorService mExecutor;
        public InternalWorker() {
            super("InternalWorker");
            mExecutor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
            start();
            // To make sure worker thread get started
            checkHandler();
        }
        
        private synchronized void checkHandler() {
            while (mHandler == null) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
        public void terminate() {
            mExecutor.shutdownNow();
            Log.e(TAG, "executor has been shutdown");
            try {
                mExecutor.awaitTermination(TERMINATION_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.e(TAG, "terminate exception caught:", e);
            }
            quit();
            mHandler = null;
        }

        @Override
        protected void onLooperPrepared() {
            mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    case MSG_DOWNLOAD:
                        doDownload((DownloadInfo) msg.obj);
                        break;
                    case MSG_STOP_SERVICE:
                	DownloadService.this.idleAndQuit();
                    default:
                        break;
                    }
                }
            };
            synchronized (this) {
                notify();
            }
        }

        private void doDownload(DownloadInfo info) {
            mExecutor.execute(new FileDownloader(info, this));
        }
        
        public void executeDownload(DownloadInfo info) {
            final Message msg = Message.obtain();
            msg.what = MSG_DOWNLOAD;
            msg.obj = info;
            mHandler.sendMessage(msg);
        }

	@Override
	public void downloadFinished() {
	    if (mHandler == null) {
		// TODO: should prevent this happening. Executor must await until all active tasks finished.
		return;
	    }
	    // When this method gets called, task is still running but about to finish. So if current only
	    // have one active task, we can know that there is only one task about to finish. It is safe to 
	    // notify service to stop itself.
	    if (((ThreadPoolExecutor) mExecutor).getActiveCount() <= 1) {
		// Change to our own thread in case block task.
		mHandler.sendEmptyMessage(MSG_STOP_SERVICE);
	    }
	}
    }
    
    private void updateNotification(int id, String filename, int current, int total, int status) {
	RemoteViews downloading = new RemoteViews("com.hilton.tigershark", R.layout.download_notification);
	downloading.setTextViewText(R.id.filename, filename);
	downloading.setProgressBar(R.id.progress, total, current, false);
	downloading.setTextViewText(R.id.status, DownloadStatus.readableStatusLabel(status));
	Intent action = new Intent();
	action.setClassName(getPackageName(), DownloadsActivity.class.getName());
	Notification noti = new Notification();
	noti.contentIntent = PendingIntent.getBroadcast(getApplication(), 0, action, 0);
	noti.contentView = downloading;
	noti.icon = android.R.drawable.stat_sys_download;
	if (status == DownloadStatus.STATUS_COMPLETED || status == DownloadStatus.STATUS_FAILED) {
	    noti.icon = android.R.drawable.stat_sys_download_done;
	} else {
	    noti.flags |= Notification.FLAG_ONGOING_EVENT;
	}
	mNotificationManager.notify(id, noti);
    }

    private class FileDownloader extends DownloadsObservable implements Runnable {
	private static final String TAG = "FileDownloader";
        private final DownloadInfo mInfo;
        private final Uri mUri;
        private int mCurrentBytes;
        private int mTotalBytes;
        final private int mId;
        
        public FileDownloader(DownloadInfo info, DownloadsObserver o) {
            mInfo = info;
            mUri = mInfo.createDatabaseRecord(getContentResolver());
            mId = Integer.valueOf(mUri.getPathSegments().get(1));
            attach(o);
        }
        
        @Override
        public void run() {
            assert(mInfo == null);
            Log.e(TAG, "downloading " + mInfo);

            final ContentValues cv = new ContentValues();
            try {
        	InputStream in = null;
        	OutputStream out = null;
        	HttpURLConnection conn = null;
        	try {
        	    URL url = new URL(mInfo.getUrl());
        	    conn = (HttpURLConnection) url.openConnection();
        	    conn.setDoInput(true);
        	    conn.setDoOutput(false);
        	    conn.connect();
        	    if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
        		Log.e(TAG, "cannot connect to server: " + conn.getResponseMessage());
        		conn.disconnect();
        		throw new IOException("cannot connect to server : " + conn.getResponseMessage());
        	    }
        	    in = new BufferedInputStream(conn.getInputStream());
        	    final String path = mInfo.getFilePath();
        	    out = new BufferedOutputStream(new FileOutputStream(path));
        	    mTotalBytes = conn.getContentLength();
        	    cv.put(Downloads.TOTAL_BYTES, mTotalBytes);
        	    cv.put(Downloads.PATH, path);
        	    cv.put(Downloads.FILENAME, mInfo.getFilename());
        	    getContentResolver().update(mUri, cv, null, null);
        	    cv.clear();
        	    byte[] buf = new byte[71926]; // 64K
        	    int seg = -999;
        	    while ((seg = in.read(buf)) != -1) {
        		out.write(buf, 0, seg);
        		mCurrentBytes += seg;
        		cv.put(Downloads.CURRENT_BYTES, mCurrentBytes);
        		getContentResolver().update(mUri, cv, null, null);
        		updateNotification(mId, mInfo.getFilename(), mCurrentBytes, mTotalBytes, DownloadStatus.STATUS_DOWNLOADING);
        		updateProgress(mId, mCurrentBytes);
        	    }
        	    cv.clear();
        	    cv.put(Downloads.STATUS, Downloads.DownloadStatus.STATUS_COMPLETED);
        	    getContentResolver().update(mUri, cv, null, null);
    		    updateNotification(mId, mInfo.getFilename(), mCurrentBytes, mTotalBytes, DownloadStatus.STATUS_COMPLETED);
        	    Log.e(TAG, "write file finished");
        	} finally {
        	    if (conn != null) {
        		conn.disconnect();
        	    }
        	    if (in != null) {
        		in.close();
        	    }
        	    if (out != null) {
        		out.close();
        	    }
        	}
            } catch (IOException e) {
        	Log.e(TAG, "exception occured ", e);
        	cv.clear();
        	cv.put(Downloads.STATUS, Downloads.DownloadStatus.STATUS_FAILED);
        	getContentResolver().update(mUri, cv, null, null);
		updateNotification(mId, mInfo.getFilename(), mCurrentBytes, mTotalBytes, DownloadStatus.STATUS_FAILED);
            }
            notifyObservers();
            detach();
        }
    }
    
    private static class ServiceStub extends IDownloadsService.Stub {
	DownloadService mService;
	
	ServiceStub(DownloadService s) {
	    mService = s;
	}

	@Override
	public int getProgress(int id) throws RemoteException {
	    return mService.getProgress(id);
	}
    }
}

class DownloadsObservable {
    private DownloadsObserver mObserver;
    
    public void attach(DownloadsObserver o) {
        mObserver = o;
    }
    
    public void detach() {
        mObserver = null;
    }
    
    public void notifyObservers() {
        if (mObserver != null) {
            mObserver.downloadFinished();
        }
    }
}

interface DownloadsObserver {
    public void downloadFinished();
}